package makai.ui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.Serializable;
import javax.swing.JComponent;
import makai.geom.ComplexArea;
import makai.ui.style.Style;
import makai.util.Toolkit;

/**
 * The color wheel in the bottom left of the Makai window.
 * 
 * This component must always strictly be 205x205 pixels!
 * 
 * TODO: Finish JavaDoc
 * 
 * @author Krechevskoy
 * @version 1.0
 */
public class ColorChooser extends JComponent implements Serializable {

    // Singleton Instance
    private static ColorChooser instance = new ColorChooser();
    
    /**
     * @return The singleton instance of the Makai Engine
     */
    public static ColorChooser getInstance(){
        return instance;
    }
    
    /**
     * Constructs a new ColorChooser
     * 
     * This should only be called once!
     */
    private ColorChooser(){
        // Full Area
        fullArea = new ComplexArea(getArc(200));
        fullArea.add(new Rectangle2D.Double(0, 0, 40, 205));
        fullArea.add(new Rectangle2D.Double(0, 190, 205, 15));
        
        // Mini Area
        miniArea = new ComplexArea(getArc(50));
        miniArea.add(new Rectangle2D.Double(0, 145, 40, 60));
        miniArea.add(new Rectangle2D.Double(0, 190, 205, 15));
        
        // Color Area
        colorArea = new ComplexArea(getArc(195));
        colorArea.subtract(getArc(186.5));
        
        colorArea.add(getArc(185));
        colorArea.subtract(getArc(50));
        
        // Foreground Area
        fgArea = new ComplexArea(getArc(45));
        fgArea.subtract(new Arc2D.Double(0, 185, 20, 20, 0, 360, Arc2D.PIE));
        fgArea.subtract(new Rectangle2D.Double(0, 185, 10, 20));
        fgArea.subtract(new Rectangle2D.Double(0, 195, 20, 10));
        
        // Background Area
        bgArea = new ComplexArea(new Arc2D.Double(5, 190, 10, 10, 0, 360, Arc2D.PIE));
        
        /*****************
         * Rounded edges *
         *****************/
        
        // Hue Area
        colorArea.subtract(new Rectangle2D.Double(5, 5, 5, 5));
        colorArea.subtract(new Rectangle2D.Double(195, 195, 5, 5));
        
        colorArea.add(getArc(10, 10, 5, 90, 90));
        colorArea.add(getArc(195, 195, 5, 270, 90));
        
        // Color Area
        colorArea.subtract(new Rectangle2D.Double(54, 195, 5, 5));
        colorArea.subtract(new Rectangle2D.Double(5, 145, 5, 5));
        
        colorArea.add(getArc(60, 195, 5, 180, 90));
        colorArea.add(getArc(10, 145, 5, 180, 90));
        
        // Foreground Area
        fgArea.subtract(new Rectangle2D.Double(5, 155, 5, 5));
        fgArea.subtract(new Rectangle2D.Double(45, 195, 5, 5));
        fgArea.subtract(new Rectangle2D.Double(20, 195, 5, 5));
        fgArea.subtract(new Rectangle2D.Double(5, 180, 5, 5));
        
        fgArea.add(getArc(10, 160.5, 5, 90, 90));
        fgArea.add(getArc(44.5, 195, 5, 270, 90));
        fgArea.add(getArc(25, 195, 5, 180, 90));
        fgArea.add(getArc(10, 180, 5, 180, 90));
        
        ChooserListener listener = new ChooserListener();
        
        addMouseListener(listener);
        addMouseMotionListener(listener);
    }
    
    // Masking Areas for painting
    private transient final ComplexArea fullArea;    // Area of the full sized color wheel
    private transient final ComplexArea miniArea;    // Area of the mini sized color wheel
    private transient final ComplexArea colorArea;   // Area of the color selector
    private transient final ComplexArea fgArea;      // Area of the foreground color
    private transient final ComplexArea bgArea;      // Area of the background color
    
    /**
     * Convenience method for defining the arcs used in this class
     * 
     * @param radius The radius of the Arc
     * 
     * @return An arc centered at (5,200) with the given radius from 0 to 90 degrees
     */
    private Arc2D.Double getArc(double radius){
        return getArc(5, 200, radius, 0, 90);
    }
    
    /**
     * Convenience method for defining the arcs used in this class
     * 
     * @param cx The central x coordinate
     * @param cy The central y coordinate
     * @param radius The radius of the Arc
     * @param angle The starting angle of the Arc (degrees)
     * @param extent The extent of the angle (degrees)
     * 
     * @return An Arc2D with the given parameters
     */
    private Arc2D.Double getArc(double cx, double cy, double radius, double angle, double extent){
        return new Arc2D.Double(cx-radius, cy-radius, radius*2, radius*2, angle, extent, Arc2D.PIE);
    }

    // The currently selected colors as an HSB array
    private float[] fgColor = {0f, 0f, 0f};
    private float[] bgColor = {0f, 0f, 1f};
    
    // Flags if the chooser should be minimized
    private boolean minimized = false;
    private boolean autoMini  = false;
    
    // Flags the chooser's currently centered hue
    private float hueCenter = 0.0f;
    
    /**
     * @return If the ColorChooser is currently minimized
     */
    public boolean isMinimized(){
        return minimized;
    }
    
    /**
     * @param mini If the color chooser should be minimized
     */
    public void isMinimized(boolean mini){
        minimized = mini;
        repaint();
    }
    
    /**
     * @return If the color chooser is set to minimize automatically
     */
    public boolean getAutoMinimize(){
        return autoMini;
    }
    
    /**
     * Sets the color chooser to automatically minimize
     */
    public void setAutoMinimize(boolean auto){
        autoMini = auto;
        isMinimized(auto);
    }
    
    /**
     * @return The hue currently centered with the color chooser
     */
    public float getCenteredHue(){
        return hueCenter;
    }
    
    /**
     * @param hueCenter The hue to be currently centered with the color chooser
     */
    public void setCenteredHue(float hueCenter){
        while(hueCenter <= -0.5f) { hueCenter += 1.0f; }
        while(hueCenter >=  0.5f) { hueCenter -= 1.0f; }
        
        this.hueCenter = hueCenter;
        
        repaint();
    }
    
    /**
     * @return The location of the sat/brightness of the current foreground color on the color chooser
     */
    public Point2D getColorLocation(){
        return getColorLocation(fgColor);
    }
    
    /**
     * @return The location of the sat/brightness of the given color on the color chooser
     */
    public Point2D getColorLocation(Color c){
        return getColorLocation(Toolkit.getHSB(c));
    }
    
    /**
     * @return The location of the sat/brightness of the given color on the color chooser
     */
    public Point2D getColorLocation(float[] hsb){
        double theta = Math.PI*hsb[1]/2.0;
        double dist  = 50 + 135*hsb[2];
        
        double x =   5 + dist * Math.cos(theta);
        double y = 200 - dist * Math.sin(theta);
        
        return new Point2D.Double(x, y);
    }
    
    /**
     * @return The location of the hue of the current foreground color on the color chooser
     */
    public Point2D getHueLocation(){
        return getHueLocation(fgColor);
    }
    
    /**
     * @return The location of the hue of the given color on the color chooser
     */
    public Point2D getHueLocation(Color c){
        return getHueLocation(Toolkit.getHSB(c));
    }
    
    /**
     * @return The location of the hue of the given color on the color chooser
     */
    public Point2D getHueLocation(float[] hsb){
        double theta = (0.5 - hueCenter - hsb[0])%1.0;
        if(theta < 0.0){ theta = 1.0+theta; }
        theta *= Math.PI/2.0;
        
        double x =   5 + 192 * Math.cos(theta);
        double y = 200 - 192 * Math.sin(theta);
        
        return new Point2D.Double(x, y);
    }
    
    /**
     * @return The color at the given location of the color chooser
     */
    public Color getColor(double x, double y){
        return getColor(x, y, true);
    }
    
    /**
     * @param preserve If the sat/brightness of the color should be preserved
     * 
     * @return The color at the given location of the color chooser
     */
    public Color getColor(double x, double y, boolean preserve){
        float hsb[] = getForegroundArray();
        
        double dist = Point.distance(5, 199, x, y);

        if(dist >= 185){
            hsb[0] = (float)(1.5-(2*Math.atan2(200-y, x-5)/Math.PI)-hueCenter);

            if(!preserve){
                hsb[1] = 1.0f;
                hsb[2] = 1.0f;
            }
        } else if(dist >= 48){
            hsb[1] = (float)(2*Math.atan2(200-y, x-5)/Math.PI);
            hsb[2] = (float)((dist-50)/135.0);
        }
        
        // Catch clipping issues
        hsb[1]=(hsb[1]<=0.0f)?0.0f:hsb[1];
        hsb[1]=(hsb[1]>=1.0f)?1.0f:hsb[1];
        
        hsb[2]=(hsb[2]<=0.0f)?0.0f:hsb[2];
        hsb[2]=(hsb[2]>=1.0f)?1.0f:hsb[2];
        
        return Toolkit.getHSBColor(hsb[0], hsb[1], hsb[2]);
    }
    
    public Color getForegroundColor(){
        return Toolkit.getHSBColor(fgColor);
    }
    
    public float[] getForegroundArray(){
        return new float[]{fgColor[0], fgColor[1], fgColor[2]};
    }
    
    public void setForegroundColor(Color c){
        fgColor = Toolkit.getHSB(c);
    }
    
    public void setForegroundColor(float[] hsb){
        fgColor = hsb;
    }
    
    public void setForegroundColor(double x, double y){
        setForegroundColor(x, y, true);
    }
    
    public void setForegroundColor(double x, double y, boolean preserve){
        double dist = Point.distance(5, 199, x, y);
        
        if(dist >= 185){
            fgColor[0] = (float)(1.5-(2*Math.atan2(200-y, x-5)/Math.PI)-hueCenter);

            if(!preserve){
                fgColor[1] = 1.0f;
                fgColor[2] = 1.0f;
            }
        } else if(dist >= 48){
            fgColor[1] = (float)(2*Math.atan2(200-y, x-5)/Math.PI);
            fgColor[2] = (float)((dist-50)/135.0);

            // Catch brightness clip near low edge
            fgColor[2]=(fgColor[2]<=0f)?0:fgColor[2];
        }
    }
    
    public Color getBackgroundColor(){
        return Toolkit.getHSBColor(bgColor);
    }
    
    public float[] getBackgroundArray(){
        return new float[]{bgColor[0], bgColor[1], bgColor[2]};
    }
    
    public void setBackgroundColor(Color c){
        bgColor = Toolkit.getHSB(c);
    }
    
    public void setBackgroundColor(float[] hsb){
        bgColor = hsb;
    }
    
    public void setBackgroundColor(double x, double y){
        setBackgroundColor(x, y, true);
    }
    
    public void setBackgroundColor(double x, double y, boolean preserve){
        double dist = Point.distance(5, 199, x, y);

        if(dist >= 185){
            bgColor[0] = (float)(1.5-(2*Math.atan2(200-y, x-5)/Math.PI)-hueCenter);

            if(!preserve){
                bgColor[1] = 1.0f;
                bgColor[2] = 1.0f;
            }
        } else if(dist >= 48){
            bgColor[1] = (float)(2*Math.atan2(200-y, x-5)/Math.PI);
            bgColor[2] = (float)((dist-50)/135.0);

            // Catch brightness clip near low edge
            bgColor[2]=(bgColor[2]<=0f)?0:bgColor[2];
        }
    }
    
    public void swapColors(){
        float[] swap = getForegroundArray();
        fgColor = bgColor;
        bgColor = swap;
    }
    
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        
        Graphics2D g2 = (Graphics2D)g;
        g2.setStroke(new BasicStroke(2));
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
        if(minimized){
            // Draw Minimized Panel
            g2.setColor(Style.panelBackground);
            g2.fill(miniArea);
        } else {
            // Draw Full Sized Panel
            g2.setColor(Style.panelBackground);
            g2.fill(fullArea);

            // Draw Colored Area
            g2.setPaint(new WheelPaint());
            g2.fill(colorArea);
                            
            // Draw Color Selection Circle
            Point2D p = getColorLocation();
            if(p != null) {
                g2.setClip(getArc(185));
                
                g2.setColor(getForegroundColor());
                g2.fill(new Ellipse2D.Double(p.getX()-9, p.getY()-9, 18, 18));
                
                g2.setColor(Style.panelBackground);
                g2.draw(new Ellipse2D.Double(p.getX()-9, p.getY()-9, 18, 18));
                g2.fill(getArc(50));
                
                g2.setClip(0, 0, getWidth(), getHeight());
            }

            // Draw Ticker
            p = getHueLocation();
            if(p != null) {
                g2.setColor(getColor(p.getX(), p.getY(), false));
                g2.fill(new Ellipse2D.Double(p.getX()-9, p.getY()-9, 18, 18));
                
                g2.setColor(Style.panelBackground);
                g2.draw(new Ellipse2D.Double(p.getX()-9, p.getY()-9, 18, 18));
            }
        }
        
        g2.setColor(getForegroundColor());
        g2.fill(fgArea);
        
        g2.setColor(getBackgroundColor());
        g2.fill(bgArea);
        
    }
    
    @Override
    public boolean contains(Point p) {
        if(minimized){
            return miniArea.contains(p);
        } else {
            return fullArea.contains(p);
        }
    }

    @Override
    public boolean contains(int x, int y) {
        if(minimized){
            return miniArea.contains(x, y);
        } else {
            return fullArea.contains(x, y);
        }
    }
    
    /**
     * Used to paint the color wheel and hue band
     */
    private class WheelPaint implements Paint {

        @Override
        public int getTransparency() {
            return java.awt.Transparency.BITMASK;
        }

        @Override
        public PaintContext createContext(  ColorModel cm,
                                            Rectangle deviceBounds,
                                            Rectangle2D userBounds,
                                            AffineTransform xform,
                                            RenderingHints hints) {
            return new Context(deviceBounds);
        }

        class Context implements PaintContext {
            
            Rectangle deviceBounds;

            public Context(Rectangle deviceBounds) {
                this.deviceBounds = deviceBounds;
            }
            
            @Override
            public void dispose() {}

            @Override
            public ColorModel getColorModel() {
               return ColorModel.getRGBdefault();
            }

            @Override
            public Raster getRaster(int xOffset, int yOffset, int w, int h) {
                WritableRaster raster = getColorModel().createCompatibleWritableRaster(w, h);
                float color[] = new float[4];
                
                xOffset -= deviceBounds.getX() - 5;
                yOffset -= deviceBounds.getY() - 5;
                
                if(minimized){ return raster; }
                
                for(int y=0; y<h; y++){
                    for(int x=0; x<w; x++){
                        
                        Color c = getColor(x+xOffset, y+yOffset, false);
                        
                        float rgb[] = Toolkit.getRGB(c);
                        color[0] = 255*rgb[0];
                        color[1] = 255*rgb[1];
                        color[2] = 255*rgb[2];
                        color[3] = 255;
                        
                        raster.setPixel(x, y, color);
                    }
                }
                
                return raster;
            }
            
        }
    }
    
    /**
     * Manages mouse events for the color chooser
     */
    private class ChooserListener implements MouseListener, MouseMotionListener {
        /**
         * Sets the type of action being currently performed:
         * 
         * 0: Hue Selection
         * 1: Hue Shifting
         * 2: FG Color Selection
         * 3: BG Color Selection
         * 4: N/A
         */
        private int type = 0;
        private float hLast = -1f;
        
        @Override
        public void mouseClicked(MouseEvent e) {
            if(fgArea.contains(e.getPoint())){
                //TODO: Show color chooser!
            } else if(bgArea.contains(e.getPoint())){
                swapColors();
                repaint();
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if(e.getButton() == MouseEvent.BUTTON2){
                isMinimized(!isMinimized());
                return;
            }
            
            if(isMinimized() || getArc(50).contains(relocate(e.getPoint()))){ return; }
            
            type = 0;
            
            if(e.getButton() == MouseEvent.BUTTON3){
                type |= 1;
            }
            
            if(getArc(185).contains(e.getPoint())) {
                type |= 2;
            }
            
            mouseDragged(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            type = 4;
            hLast = -1f;
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            if(autoMini){
                isMinimized(false);
            }
        }

        @Override
        public void mouseExited(MouseEvent e) {
            if(autoMini){
                isMinimized(true);
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            Point2D p = relocate(e.getPoint());
            
            double dist = p.distance(5, 200);
            
            switch(type){
                case 0:
                    p = relocate(p, 192);
                    
                    setForegroundColor(p.getX(), p.getY());
                    break;
                case 1:
                    if(hLast >= 0){
                        setCenteredHue(hueCenter + hLast - getHueTheta(e.getPoint()));
                    }
                    
                    hLast = getHueTheta(e.getPoint());
                    break;
                case 2:
                    if(dist > 185){ p = relocate(p, 185); }
                    if(dist < 50){ p = relocate(p, 50); }
                    
                    setForegroundColor(p.getX(), p.getY());
                    break;
                case 3:
                    if(dist > 185){ p = relocate(p, 185); }
                    if(dist < 50){ p = relocate(p, 50); }
                    
                    if(dist <= 185 && dist > 45){
                        setBackgroundColor(p.getX(), p.getY());
                    }
                    break;
                default:
                    return;
            }
            repaint();
        }

        @Override
        public void mouseMoved(MouseEvent e) {}
        
        /**
         * This calculation is simplified by assumptions, and as such, is for listener use only
         * 
         * @return The relative hue "angle"... 
         */
        private float getHueTheta(Point p){
            return (float)(2*Math.atan2(200-p.y, p.x-5)/Math.PI);
        }
        
        private Point2D relocate(Point p){
            double x = Math.max(p.getX(), 5);
            double y = Math.min(p.getY(), 200);
            
            return new Point2D.Double(x, y);
        }
        
        private Point2D relocate(Point2D p, double dist){
            double x = Math.max(p.getX(), 5);
            double y = Math.min(p.getY(), 200);
            
            double theta = Math.atan2(200-y, x-5);
            
            x = 5   + dist*Math.cos(theta);
            y = 200 - dist*Math.sin(theta);
            
            return new Point2D.Double(x, y);
        }
        
    }
}

